﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;

namespace BizElements.Core
{
    /// <summary>Extends <see cref="IEnumerable"/> and <see cref="IEntityCollection"/> collections.</summary>
    public static class EntityCollectionExtensions
    {
        #region GetByPK.

        /// <summary>
        /// Gets the first entity with the given primary key value.
        /// </summary>
        /// <param name="entityCollection">Collection of <see cref="IEntity"/> objects.</param>
        /// <param name="primaryKeyValue">Primary key values in order as returned by <see cref="EntityExtensions.GetPrimaryKeyValue(IEntity)"/> method.</param>
        /// <returns>Entity with the given key or <b>null</b> if it cannot be found.</returns>
        public static TEntity GetByPK<TEntity>(this IEnumerable<TEntity> entityCollection, params object[] primaryKeyValue)
            where TEntity : class, IEntity
        {
            foreach (TEntity entity in entityCollection)
            {
                if (entity.PrimaryKeyEquals(primaryKeyValue))
                    return entity;
            }

            return null;
        }

        /// <summary>
        /// Gets the first entity with the given primary key value.
        /// </summary>
        /// <param name="entityCollection">Collection of <see cref="IEntity"/> objects.</param>
        /// <param name="primaryKeyValue">Primary key values in order as returned by <see cref="EntityExtensions.GetPrimaryKeyValue(IEntity)"/> method.</param>
        /// <returns>Entity with the given key or <b>null</b> if it cannot be found.</returns>
        public static IEntity GetByPK(this IEnumerable entityCollection, object[] primaryKeyValue)
        {
            foreach (IEntity entity in entityCollection)
            {
                if (entity.PrimaryKeyEquals(primaryKeyValue))
                    return entity;
            }

            return null;
        }

        #endregion

        #region SetParents.

        /// <summary>
        /// Sets the parent defined by the given relation for all entities in the collection.
        /// </summary>
        /// <param name="entityCollection">Collection of <see cref="IEntity"/> objects.</param>
        /// <param name="foreignKey">FK.</param>
        /// <param name="parent">Parent entity.</param>
        /// <remarks>All child entities in the collection will share the same parent object instance (same reference).</remarks>
        public static void SetParents(this IEnumerable entityCollection, DbRelation foreignKey, IEntity parent)
        {
            foreach (IEntity child in entityCollection)
                child.SetParent(foreignKey, parent);
        }

        /// <summary>
        /// Sets the parent defined by the given relation for all entities in the collection.
        /// </summary>
        /// <param name="entityCollection">Collection of <see cref="IEntity"/> objects.</param>
        /// <param name="foreignKey">FK.</param>
        /// <param name="parent">Parent entity.</param>
        /// <param name="shareParentObject">Indicates if all child entities in the collection will share the same 
        /// parent object instance (same reference). If set to <b>false</b>, a clone of the parent will be created 
        /// for each child entity in the current collection.</param>
        public static void SetParents(this IEnumerable entityCollection, DbRelation foreignKey, IEntity parent, bool shareParentObject)
        {
            if (shareParentObject)
            {
                SetParents(entityCollection, foreignKey, parent);
            }
            else
            {
                foreach (IEntity child in entityCollection)
                {
                    IEntity privateParentInstance = (parent != null) ? (IEntity)parent.Clone() : null;
                    child.SetParent(foreignKey, privateParentInstance);
                }
            }
        }

        /// <summary>
        /// Sets the parent defined by the given relation for all entities in the collection.
        /// </summary>
        /// <param name="entityCollection">Collection of <see cref="IEntity"/> objects.</param>
        /// <param name="foreignKey">FK.</param>
        /// <param name="parents">Collection of all parent entities referenced by child entities in the current 
        /// collection. If any child references a parent which is not provided, an exception will be generated.</param>
        /// <remarks><p>The method will try to find a parent in the provided collection for each child entity 
        /// in the current collection. Child whose FK is not set are skipped. If any child references a parent 
        /// which is not provided, an exception will be generated. If a child's FK is invalid, i.e. only partially 
        /// set, an exception will be generated.</p>
        /// <p>Child entities with equal FK values will share the same parent object instance (same reference).</p></remarks>
        public static void SetParents(this IEnumerable entityCollection, DbRelation foreignKey, IEnumerable parents)
        {
            foreach (IEntity child in entityCollection)
            {
                object[] parentPk = PkUtil.CreatePkFromFk(child, foreignKey);
                if (parentPk != null)
                {
                    IEntity currParent = GetByPK(parents, parentPk);
                    if (currParent == null)
                        throw new InvalidOperationException(Messages.EntityCollection_SetParents_InvalidFk);

                    child.SetParent(foreignKey, currParent);
                }
            }
        }

        /// <summary>
        /// Sets the parent defined by the given relation for all entities in the collection.
        /// </summary>
        /// <param name="entityCollection">Collection of <see cref="IEntity"/> objects.</param>
        /// <param name="foreignKey">FK.</param>
        /// <param name="parents">Collection of all parent entities referenced by child entities in the current 
        /// collection. If any child references a parent which is not provided, an exception will be generated.</param>
        /// <param name="shareParentObject">Indicates if child entities with equal FK values will share the same
        /// parent object instance (same reference). If set to <b>false</b>, a clone of a parent will be created
        /// for each child entity in the current collection.</param>
        /// <remarks><p>The method will try to find a parent in the provided collection for each child entity 
        /// in the current collection. Child whose FK is not set are skipped. If any child references a parent 
        /// which is not provided, an exception will be generated. If a child's FK is invalid, i.e. only partially 
        /// set, an exception will be generated.</p></remarks>
        public static void SetParents(this IEnumerable entityCollection, DbRelation foreignKey, IEnumerable parents, bool shareParentObject)
        {
            if (shareParentObject)
            {
                SetParents(entityCollection, foreignKey, parents);
            }
            else
            {
                foreach (IEntity child in entityCollection)
                {
                    object[] parentPk = PkUtil.CreatePkFromFk(child, foreignKey);
                    if (parentPk != null)
                    {
                        IEntity currParent = GetByPK(parents, parentPk);
                        if (currParent == null)
                            throw new InvalidOperationException(Messages.EntityCollection_SetParents_InvalidFk);

                        child.SetParent(foreignKey, (IEntity)currParent.Clone());
                    }
                }
            }
        }

        #endregion

        #region ToDataTable.

        /// <summary>Converts the collection to DataTable.</summary>
        /// <param name="collection">Entity collection.</param>
        /// <returns>Data.</returns>
        /// <remarks><para>Conversion will fail if the collection contains entities whose non-nullable fields
        /// haven't been initialized or if it contains multiple entities with the same primary key.</para>
        /// <para>All rows will have <see cref="DataRowState.Added"/> state.</para></remarks>
        public static DataTable ToDataTable(this IEntityCollection collection)
        {
            return ToDataTable(collection, collection.IDbTable);
        }

        /// <summary>Converts the collection to DataTable.</summary>
        /// <param name="collection">A collection of <see cref="IEntity"/> objects.</param>
        /// <param name="meta">Metada for table in which the entities in the provided collection are stored in.</param>
        /// <returns>Data.</returns>
        /// <remarks><para>Conversion will fail if the collection contains entities whose non-nullable fields
        /// haven't been initialized or if it contains multiple entities with the same primary key.</para>
        /// <para>All rows will have <see cref="DataRowState.Added"/> state.</para></remarks>
        public static DataTable ToDataTable(this IEnumerable collection, IDbTable meta)
        {
            DataTable data = meta.NewDataTable();
            data.BeginLoadData();
            foreach (IEntity entity in collection)
            {
                object[] rowData = GetRowData(entity);
                data.Rows.Add(rowData);
            }

            data.EndLoadData();
            return data;
        }

        private static object[] GetRowData(IEntity entity)
        {
            object[] values = entity.ToObjectArray();
            for (int i = 0; i < values.Length; i++)
            {
                if (values[i] == null)
                    values[i] = DBNull.Value;
            }

            return values;
        }

        /// <summary>
        /// Converts the collection to DataTable.
        /// </summary>
        /// <param name="collection">Entity collection.</param>
        /// <param name="convertEntityStatesToDataRowStates">Indicates whether the <see cref="DataRow.RowState"/>
        /// of the rows contained in the DataTable is to be set to values compatibile to the values contained 
        /// in the <see cref="IEntity.EntityState"/> properties of the entities contained in the collection. 
        /// If set to <b>false</b> all rows will have <see cref="DataRowState.Added"/> state.</param>
        /// <returns>Data.</returns>
        /// <remarks><para>Conversion will fail if the collection contains entities whose non-nullable fields
        /// haven't been initialized or if it contains multiple entities with the same primary key.</para>
        /// <list type="bullet">
        /// <listheader>EntityState to DataRowState conversion rules</listheader>
        /// <item>
        ///		<term><see cref="EntityState.Deleted"/></term>
        ///		<description>The row is not added to the DataTable.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.New"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Added"/>.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.OutOfSync"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Modified"/>.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.PendingDeletion"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Deleted"/>.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.Synchronized"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Unchanged"/>.</description>
        ///	</item>
        /// </list></remarks>
        public static DataTable ToDataTable(this IEntityCollection collection, bool convertEntityStatesToDataRowStates)
        {
            return ToDataTable(collection, collection.IDbTable, convertEntityStatesToDataRowStates);
        }

        /// <summary>
        /// Converts the collection to DataTable.
        /// </summary>
        /// <param name="collection">A collection of <see cref="IEntity"/> objects.</param>
        /// <param name="meta">Metada for table in which the entities in the provided collection are stored in.</param>
        /// <param name="convertEntityStatesToDataRowStates">Indicates whether the <see cref="DataRow.RowState"/>
        /// of the rows contained in the DataTable is to be set to values compatibile to the values contained 
        /// in the <see cref="IEntity.EntityState"/> properties of the entities contained in the collection. 
        /// If set to <b>false</b> all rows will have <see cref="DataRowState.Added"/> state.</param>
        /// <returns>Data.</returns>
        /// <remarks><para>Conversion will fail if the collection contains entities whose non-nullable fields
        /// haven't been initialized or if it contains multiple entities with the same primary key.</para>
        /// <list type="bullet">
        /// <listheader>EntityState to DataRowState conversion rules</listheader>
        /// <item>
        ///		<term><see cref="EntityState.Deleted"/></term>
        ///		<description>The row is not added to the DataTable.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.New"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Added"/>.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.OutOfSync"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Modified"/>.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.PendingDeletion"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Deleted"/>.</description>
        ///	</item>
        ///	<item>
        ///		<term><see cref="EntityState.Synchronized"/></term>
        ///		<description><see cref="DataRow.RowState"/> property is set to <see cref="DataRowState.Unchanged"/>.</description>
        ///	</item>
        /// </list></remarks>
        public static DataTable ToDataTable(this IEnumerable collection, IDbTable meta, bool convertEntityStatesToDataRowStates)
        {
            if (!convertEntityStatesToDataRowStates)
            {
                return ToDataTable(collection, meta);
            }
            else
            {
                DataTable data = meta.NewDataTable();
                data.BeginLoadData();
                foreach (IEntity entity in collection)
                {
                    object[] rowData = GetRowData(entity);
                    data.Rows.Add(rowData);
                    DataRow row = data.Rows[data.Rows.Count - 1];

                    // Set DataRowState to value which is compatibile to the EntityState. 
                    // Default is Added. Requires hacks for Modified, Deleted and Unchanged.
                    switch (entity.EntityState)
                    {
                        case EntityState.Deleted:
                            // Remove from DataTable.
                            row.AcceptChanges();
                            row.Delete();
                            row.AcceptChanges();
                            break;

                        case EntityState.New:
                            // Do nothing. DataRowState is already set to Added.
                            break;

                        case EntityState.OutOfSync:
                            // Convert to Modified.
                            row.AcceptChanges();
                            row[0] = row[0];
                            break;

                        case EntityState.PendingDeletion:
                            // Convert to Deleted.
                            row.AcceptChanges();
                            row.Delete();
                            break;

                        case EntityState.Synchronized:
                            // Convert to Unchanged.
                            row.AcceptChanges();
                            break;
                    }
                }

                data.EndLoadData();
                return data;
            }
        }

        #endregion

        #region FromDataTable.

        /// <summary>
        /// Clears the EntityCollection and fills it with entities created from the given data.
        /// </summary>
        /// <param name="entityCollection">A collection of <see cref="IEntity"/> objects.</param>
        /// <param name="data">DataTable containing data for entities.</param>
        public static void FromDataTable(this IEntityCollection entityCollection, DataTable data)
        {
            entityCollection.Clear();
            foreach (DataRow row in data.Rows)
            {
                IEntity newEntity = entityCollection.IDbTable.NewEntity();
                newEntity.FromDataRow(row);
                // Add directly to wrapped collection to skip unnecessary validation.
                entityCollection.Add(newEntity, false);
            }
        }

        /// <summary>
        /// Clears the EntityCollection and fills it with entities created from the given data.
        /// </summary>
        /// <param name="entityCollection">A collection of <see cref="IEntity"/> objects.</param>
        /// <param name="data">DataTable containing data for entities.</param>  
        /// <param name="fieldMetadata"><see cref="IDbTable"/> meta data object which links ADO.NET row columns to entity properties.</param>
        /// <remarks>This method, when used in combination with <see cref="IDbTable"/> objects that contain columns with
        /// prefixed aliases, enables you to initialize multiple entities that belong to different tables with data from a single DataRow.</remarks>
        public static void FromDataTable(this IEntityCollection entityCollection, DataTable data, IDbTable fieldMetadata)
        {
            entityCollection.Clear();
            foreach (DataRow row in data.Rows)
            {
                IEntity newEntity = entityCollection.IDbTable.NewEntity();
                newEntity.FromDataRow(row, fieldMetadata);
                // Add directly to wrapped collection to skip unnecessary validation.
                entityCollection.Add(newEntity, false);
            }
        }

        /// <summary>
        /// Clears the EntityCollection and fills it with entities created from the given data.
        /// </summary>
        /// <param name="entityCollection">A collection of <see cref="IEntity"/> objects.</param>
        /// <param name="data">DataTable containing data for entities.</param>
        public static void FromDataTable<TEntity>(this IList entityCollection, DataTable data)
            where TEntity : IEntity, new()
        {
            entityCollection.Clear();
            foreach (DataRow row in data.Rows)
            {
                TEntity newEntity = new TEntity();
                newEntity.FromDataRow(row);
                entityCollection.Add(newEntity);
            }
        }

        /// <summary>
        /// Clears the EntityCollection and fills it with entities created from the given data.
        /// </summary>
        /// <param name="entityCollection">A collection of <see cref="IEntity"/> objects.</param>
        /// <param name="data">DataTable containing data for entities.</param>  
        /// <param name="fieldMetadata"><see cref="IDbTable"/> meta data object which links ADO.NET row columns to entity properties.</param>
        /// <remarks>This method, when used in combination with <see cref="IDbTable"/> objects that contain columns with
        /// prefixed aliases, enables you to initialize multiple entities that belong to different tables with data from a single DataRow.</remarks>
        public static void FromDataTable<TEntity>(this IList entityCollection, DataTable data, IDbTable fieldMetadata)
            where TEntity : IEntity, new()
        {
            entityCollection.Clear();
            foreach (DataRow row in data.Rows)
            {
                TEntity newEntity = new TEntity();
                newEntity.FromDataRow(row, fieldMetadata);
                entityCollection.Add(newEntity);
            }
        }

        #endregion
    }
}
